/*
 * Copyright (c) 2004, 2012, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.oracle.max.vma.tools.qa;

import static com.oracle.max.vm.ext.vma.store.txt.VMATextStoreFormat.*;
import static com.oracle.max.vma.tools.qa.TransientVMAdviceHandlerTypes.*;
import static com.oracle.max.vma.tools.qa.TransientVMAdviceHandlerTypes.RecordType.*;

import java.io.*;
import java.util.*;

import com.oracle.max.vm.ext.vma.*;
import com.oracle.max.vm.ext.vma.store.*;
import com.oracle.max.vm.ext.vma.store.txt.*;
import com.oracle.max.vma.tools.log.*;
import com.oracle.max.vma.tools.log.ConvertLog.MergeCommand.PushRecord;
import com.sun.max.program.*;

/**
 * Reads a log file in the format generated by {@link CSFVMATextStore}.
 *
 * Object identifiers in the log may be reused owing to garbage collection. Object identifiers in the {@link #objects}
 * map are unique and always qualified by the allocation (gc) epoch, {@link #allocationEpoch). I.e., an id X in the log is
 * stored as "X:allocationEpoch", e.g. "X:0, X:3, ..."
 *
 * Note that although the map may contain many instances of X:n, at any point in time there can only be one active
 * instance, which is the value of "n" given by the {@link #maxAllocationEpoch} map.
 *
 * The log must be time ordered for the main body of the code to work properly.
 * An unordered log is detected automatically and converted using {@link ConvertLog}.
 *
 *
 */
public class ProcessLog {

    static class TraceException extends Exception {

        static final long serialVersionUID = 0;

        TraceException(String s) {
            super(s);
        }
    }

    public static abstract class RecordReader {
        public abstract String[] readLine() throws IOException;
        public abstract void close() throws IOException;
    }

    private static class BufferedRecordReader extends RecordReader {
        private BufferedReader reader;

        BufferedRecordReader(BufferedReader reader) {
            this.reader = reader;
        }

        @Override
        public String[] readLine() throws IOException {
            String line = reader.readLine();
            if (line == null) {
                return null;
            }
            return ConvertLog.split(textKeyMode, line);
        }

        @Override
        public void close() throws IOException {
            reader.close();
        }
    }

    private static class PushReader extends RecordReader implements PushRecord {
        private static final int LENGTH = 1024;
        private String[][]lineParts = new String[LENGTH][];
        private int count;
        private int writeIndex;
        private int readIndex;

        @Override
        public synchronized String[] readLine() throws IOException {
            while (count == 0) {
                try {
                    wait();
                } catch (InterruptedException ex) {
                }
            }
            count--;
            notify();
            String[] result = lineParts[readIndex];
            readIndex = (readIndex + 1) % LENGTH;
            return result;
        }

        @Override
        public void close() throws IOException {
        }

        @Override
        public synchronized void pushRecord(String[] lineParts) {
            while (count >= LENGTH) {
                try {
                    wait();
                } catch (InterruptedException ex) {
                }
            }
            this.lineParts[writeIndex] = lineParts;
            writeIndex = (writeIndex + 1) % LENGTH;
            count++;
            notify();
        }

    }

    private static class PushReaderThread extends Thread {
        File[] files;
        PushReader pushReader;

        PushReaderThread(File[] files, PushReader pushReader) {
            this.pushReader = pushReader;
            this.files = files;
            setName("PushReader");
        }

        @Override
        public void run() {
            ConvertLog.MergeCommand command = new ConvertLog.MergeCommand(pushReader);
            command.execute(files, null);
        }

    }

    /**
     * Uniquely identifies a class by its name and classloader id.
     */
    static class ClassNameId implements Comparable {

        String className;
        String clId;

        ClassNameId(String name, String id) {
            this.className = name;
            this.clId = id;
        }

        public int compareTo(Object other) {
            ClassNameId otherClassName = (ClassNameId) other;
            if (otherClassName.className.equals(className)) {
                return clId.compareTo(otherClassName.clId);
            } else {
                return className.compareTo(otherClassName.className);
            }
        }

        @Override
        public int hashCode() {
            return className.hashCode() ^ clId.hashCode();
        }

        @Override
        public boolean equals(Object other) {
            return compareTo(other) == 0;
        }

        @Override
        public String toString() {
            return className + "(cl:" + clId + ")";
        }
    }

    private boolean verbose = false;
    private int maxLines;

    /**
     * Experimentally determined, using {@code cv -stats}, used to set the size of the {@link #adviceRecordList}.
     */
    private static final int AVG_LINE_LENGTH = 12;  //

    private Map<String, ObjectRecord> objects = new HashMap<String, ObjectRecord>(1024 * 1024);

    private SortedMap<ClassNameId, ClassRecord> classMap = new TreeMap<ClassNameId, ClassRecord>();
    private Map<String, ThreadRecord> threadMap = new HashMap<String, ThreadRecord>();
    private Map<String, FieldRecord> fieldMap = new HashMap<String, FieldRecord>();
    private Map<String, MethodRecord> methodMap = new HashMap<String, MethodRecord>();

    private ArrayList<AllocationEpoch> allocationEpochs = new ArrayList<AllocationEpoch>();

    private long objectCount = 0;
    private long arrayCount = 0;

    private int missingConstructorCount = 0;

    private long startTime;   // absolute value in trace
    private long lastTime;    // absolute value computed from startTime + increments in the trace records
    private boolean absTime;  // if log uses absolute time

    /**
     * bytecode index of associated bytecode instruction.
     */
    private short bci;

    private AllocationEpoch allocationEpoch;
    private AllocationEpoch prevAllocationEpoch;
    /**
     * Key is an unqualified id and the value is the last allocation epoch that id was defined (allocated) in.
     * I.e., to get a qualified id, look up the value mapped from the unqualified id and append it.
     */
    private Map<String, Integer> maxAllocationEpoch = new HashMap<String, Integer>();
    private int startRemoval = -1;
    private int endRemoval;

    private Map<String, String> lastId = new HashMap<String, String>();

    // Following fields hold the values for the current trace (if appropriate)
    private ObjectRecord objectRecord;
    private FieldRecord fieldRecord;
    private MethodRecord methodRecord;
    private ThreadRecord threadRecord;
    private ClassRecord classRecord;

    private Map<String, ObjectRecord> missingConstructors = new HashMap<String, ObjectRecord>();

    /* These maps hold forward references that can occur in logs that were generated by
     * the per-thread batching generator. Even when time ordered, one thread may use
     * a short form that is defined later in another thread. This is because there is
     * no relationship between the time the thread buffer is flushed and the time the
     * short form is created.
     */

    /**
     *  A map from the class short form to the full name and class loader id.
     */
    private Map<String, ClassNameId> classShortFormsToFull = new HashMap<String, ClassNameId>();
    /**
     * A map from the thread short form to the full name.
     */
    private Map<String, String> threadShortFormsToFull = new HashMap<String, String>();

    /**
     * Records the forward references to class short forms, allowing a placeholder
     * {@link ClassRecord} to be created using a fake {@link ClassNameId} which
     * can be fixed up later when the definition is encountered.
     */
    private Map<String, ClassNameId> classForwardRefs = new HashMap<String, ClassNameId>();
    private static final String FORWARD_PREFIX = "Forward:";

    /*
     * Fields/methods share common behavior so we have a generic handler class.
     *
     */
    private interface Factory<T> {
        T create(ClassRecord cr, String name);
    }

    private static class FieldRecordFactory implements Factory<FieldRecord> {
        @Override
        public FieldRecord create(ClassRecord cr, String name) {
            return new FieldRecord(cr, name);
        }
    }

    private static class MethodRecordFactory implements Factory<MethodRecord> {
        @Override
        public MethodRecord create(ClassRecord cr, String name) {
            return new MethodRecord(cr, name);
        }
    }

    private static class QualName {
        ClassRecord cr;
        String name;
        QualName(ClassRecord cr, String name) {
            this.cr = cr;
            this.name = name;
        }
    }


    private class ShortFormHandler<T extends MemberRecord> {
        /**
         * The key is the short form and the value (when defined) is the actual name.
         */
        private Map<String, QualName> shortFormToFull = new HashMap<String, QualName>();
        /**
         * Set of forward references.
          */
        private Set<String> forwardRefs = new HashSet<String>();
        /**
         * Key is short name (which is unique, unlike the member name); value is an instance of T,
         * but {@code classRecord} field may be a forward ref also if the class has not yet been defined.
         */
        private Map<String, T> kindMap = new HashMap<String, T>();
        /**
         * factory for creating instances of {@link T}.
         */
        private final Factory<T> factory;

        ShortFormHandler(String kind, Factory<T> factory) {
            this.factory = factory;
        }

        /**
         * Gets an instance of T for short form arguments.
         * If these short forms have already been defined then the result
         * will not need fixing up later.
         * @param shortName
         */
        T getRecord(String shortName) {
            QualName qualName = shortFormToFull.get(shortName);
            if (qualName == null) {
                // forward reference
                if (!forwardRefs.contains(shortName)) {
                    // new forward reference
                    forwardRefs.add(shortName);
                }
            }
            T kindRecord = kindMap.get(shortName);
            if (kindRecord == null) {
                kindRecord = factory.create(qualName == null ? null : qualName.cr, qualName == null ? null : qualName.name);
                kindMap.put(shortName, kindRecord);
            }
            return kindRecord;
        }

        /**
         * Definition of a member (field/method).
         * N.B. The definition uses a class short form which may not yet be defined.
         */
        void define() {
            String classShortForm = recordParts[DEFINE_ARG_INDEX];
            ClassRecord cr = getClassRecord(classShortForm); // may be forward
            String definition = recordParts[DEFINE_ARG_INDEX + 1];
            String shortName = recordParts[DEFINE_ARG_INDEX + 2];
            shortFormToFull.put(shortName, new QualName(cr, definition));
            T t;
            if (forwardRefs.contains(shortName)) {
                t = kindMap.remove(shortName);
                assert t != null;
                t.setName(definition);
                t.setClassRecord(cr);
            } else {
                // no forward refs to this member
                t = factory.create(cr, definition);
            }
            kindMap.put(shortName, t);
        }

    }

    private ShortFormHandler<FieldRecord> fieldShortFormHandler = new ShortFormHandler<FieldRecord>("Field", new FieldRecordFactory());
    private ShortFormHandler<MethodRecord> methodShortFormHandler = new ShortFormHandler<MethodRecord>("Method", new MethodRecordFactory());

    /**
     * List of all the records in the trace using the same format as {@link TransientVMAdviceHandlerTypes}.
     */
    private ArrayList<AdviceRecord> adviceRecordList;

    /**
     * The parts of a log record split at space boundaries.
     */
    private String[] recordParts;
    private int lineNumber;

    private static boolean textKeyMode;

    private ProcessLog(boolean verbose, int maxLines) throws IOException {
        this.verbose = verbose;
        this.maxLines = maxLines;
    }

    public static TraceRun processTrace(String dataDir, boolean verbose, int maxLines) throws IOException {
        ProcessLog pt = new ProcessLog(verbose, maxLines);
        return pt.doProcessTrace(dataDir);
    }

    private int estimateRecordCount(File[] files) {
        int length = 0;
        for (File file : files) {
            length += file.length();
        }
        return length / AVG_LINE_LENGTH;
    }

    private TraceRun doProcessTrace(String dataDirName) throws IOException {
        long chunkStartTime = System.currentTimeMillis();
        long processStartTime = chunkStartTime;

        File dataDir = new File(dataDirName);
        File dataFile =  null;
        if (dataDir.isDirectory()) {
            dataFile = new File(dataDir, VMAStoreFile.GLOBAL_STORE);
        } else {
            dataFile = dataDir;
        }

        int adviceRecordListCountEstimate = 0;
        RecordReader reader = null;

        if (dataFile.exists()) {
            adviceRecordListCountEstimate = estimateRecordCount(new File[] {dataFile});
            reader = checkTimeOrdered(dataFile);
        } else {
            // either a per-thread store or an error
            if (dataDir.isDirectory()) {
                adviceRecordListCountEstimate = estimateRecordCount(dataDir.listFiles());
                PushReader pushReader = new PushReader();
                reader = pushReader;
                new PushReaderThread(dataDir.listFiles(), pushReader).start();
            } else {
                throw new FileNotFoundException(dataDirName);
            }
        }


        if (verbose) {
            System.out.println("processing trace file " + dataDirName + " starting");
        }

        adviceRecordList = new ArrayList<AdviceRecord>(adviceRecordListCountEstimate);

        lineNumber = 1;
        boolean checked = false;
        try {
            while (true) {
                recordParts = reader.readLine();
                if (recordParts == null) {
                    break;
                }
                if (recordParts.length == 0 || recordParts[0].charAt(0) == '#') {
                    continue;
                }
                if (!checked) {

                    checked = true;
                }
                try {
                    processTraceRecord();
                } catch (TraceException e) {
                    System.err.println("line " + lineNumber + ": " + e);
                }
                lineNumber++;
                if (lineNumber >= maxLines) {
                    System.out.println("reached max line count - terminating processing");
                    break;
                }
                if (verbose && ((lineNumber % 100000) == 0)) {
                    long endTime = System.currentTimeMillis();
                    System.out.printf("processed %d traces in %d ms (%d)%n", lineNumber, endTime - processStartTime, endTime - chunkStartTime);
                    chunkStartTime = endTime;
                }
            }
        } finally {
            reader.close();
        }

        if (verbose) {
            System.out.println("processing trace file " + dataDirName + " complete");
        }

        checkSorted();

        // create a map from classloaders to classes loaded by them
        Map<String, SortedMap<String, ClassRecord>> classLoaders = new HashMap<String, SortedMap<String, ClassRecord>>();
        for (ClassRecord cr : classMap.values()) {
            SortedMap<String, ClassRecord> clMap = classLoaders.get(cr.getClassLoaderId());
            if (clMap == null) {
                clMap = new TreeMap<String, ClassRecord>();
                classLoaders.put(cr.getClassLoaderId(), clMap);
            }
            clMap.put(cr.getName(), cr);
        }

        fixupEndCreationRecords();

        TraceRun result = new TraceRun(dataDirName, adviceRecordList, objects, threadMap, classLoaders,
                        missingConstructors, objectCount, arrayCount,
                        missingConstructorCount, allocationEpochs, startTime, lastTime);

        return result;
    }

    private void checkSorted() {
        AdviceRecord last = null;
        for (int i = 0; i < adviceRecordList.size(); i++) {
            AdviceRecord ar = adviceRecordList.get(i);
            if (last != null) {
                if (last.time > ar.time) {
                    System.err.println("advice record list is not sorted by time, at record " + i);
                    System.exit(1);
                }
            }
            last = ar;
        }
    }

    /**
     * Check that the header line is not corrupt and set the key mode.
     */
    private void checkStoreHeader() {
        assert recordParts.length == 4;
        textKeyMode = (Integer.parseInt(recordParts[3]) & TEXT_KEY) != 0;
        assert VMATextStoreFormat.getCommand(textKeyMode, recordParts[0]) == Key.INITIALIZE_STORE;
    }

    private RecordReader checkTimeOrdered(File file) throws IOException {
        BufferedRecordReader reader = new BufferedRecordReader(new BufferedReader(new FileReader(file)));
        recordParts = reader.readLine();
        checkStoreHeader();
        reader.close();
        int mode = Integer.parseInt(recordParts[3]);
        if ((mode & BATCHED) != 0) {
            // not time ordered, run the converter to a temp file
            if (verbose) {
                System.out.println("creating time ordered log from per-thread batched log");
            }
            File tempFile = File.createTempFile("vma", null);
            try {
                ConvertLog.main(new String[] {"-f", file.getAbsolutePath(), "-o", tempFile.getAbsolutePath(), "-unbatch"});
            } catch (IOException ex) {
                throw ex;
            } catch (Exception ex) {
                ProgramError.unexpected("failed to convert: " + file.getAbsolutePath(), ex);
            }
            file = tempFile;
        }
        return new BufferedRecordReader(new BufferedReader(new FileReader(file)));
    }

    /**
     * Fixup the end creation record for object records.
     * If the trace has INVOKESPECIAL/AFTER records, this just means fixing up
     * the case where the AFTER <init> was a forward reference.
     *
     * Otherwise, where we only have METHOD_ENTRY/RETURN pairs it means locating
     * the matching RETURN for the <init> after the NEW.
     */
    private void fixupEndCreationRecords() {
        for (ObjectRecord objectRecord : objects.values()) {
            if (objectRecord.endCreationRecord == null) {
                /* TODO depends on INVOKE AFTER advice
                // we scan backwards to find the last <init> return
                for (int i = adviceRecordList.size() - 1; i > 0; i--) {
                    AdviceRecord ar = adviceRecordList.get(i);
                    RecordType rt = ar.getRecordType();
                    if (rt == InvokeSpecial && ar.getAdviceMode() == AdviceMode.AFTER.ordinal()) {
                        ObjectRecord invokeObject = AdviceRecordHelper.getObjectRecord(ar);
                        if (objectRecord.id.equals(invokeObject.id)) {
                            MethodRecord mr = AdviceRecordHelper.getMethod(ar);
                            if (mr.name.equals("<init>")) {
                                objectRecord.setEndCreationRecord(ar);
                            }
                        }
                    }
                }
                */
                int index = getRecordListIndex(objectRecord.beginCreationRecord);
                assert index >= 0 : "failed to find creation record index";
                int mIndex = getInitMethodEntry(objectRecord, index + 1);
                // We define end creation to be the RETURN that matches this constructor invocation.
                // There may be an arbitrary number of other method invocations in between
                AdviceRecord endCreationRecord = null;
                if (mIndex > 0) {
                    int depth = 0;
                    for (int i = mIndex + 1; i < adviceRecordList.size(); i++) {
                        AdviceRecord ar = adviceRecordList.get(i);
                        RecordType art = ar.getRecordType();
                        if (art == MethodEntry) {
                            depth++;
                        } else if (art == Return || art == ReturnDouble || art == ReturnFloat || art == ReturnLong || art == ReturnObject || art == ReturnByThrow) {
                            if (depth == 0) {
                                endCreationRecord = ar;
                                break;
                            } else {
                                if (art == ReturnByThrow) {
                                    int pop = ar.getPackedValue();
                                    depth -= pop;
                                } else {
                                    depth--;
                                }
                            }
                        }
                    }
                } else {
                    endCreationRecord = adviceRecordList.get(index + 1);
                }
                if (endCreationRecord == null) {
                    System.err.printf("failed to find end creation record for %s%n", objectRecord);
                    endCreationRecord = adviceRecordList.get(index + 1);
                }
                objectRecord.setEndCreationRecord(endCreationRecord);
            }
        }
    }

    /**
     * Attempt to locate the constructor METHOD_ENTRY for {@code objectRecord} starting at {@code index}.
     * There may not be one if the class was not instrumented.
     * @param objectRecord
     * @param index
     * @return index of METHOD_ENTRY revcord or -1 of not found
     */
    private int getInitMethodEntry(ObjectRecord objectRecord, int index) {
        for (int i = index; i < adviceRecordList.size(); i++) {
            AdviceRecord ar = adviceRecordList.get(i);
            if (ar.getRecordType() == MethodEntry) {
                ObjectRecord methodEntryObject = AdviceRecordHelper.getObjectRecord(ar);
                if (methodEntryObject != null && objectRecord.id.equals(methodEntryObject.id)) {
                    MethodRecord mr = AdviceRecordHelper.getMethod(ar);
                    if (mr.name.equals("<init>")) {
                        return i;
                    }
                }
            }
        }
        return -1;
    }

    private int getRecordListIndex(AdviceRecord ar) {
        return AdviceRecordHelper.getRecordListIndex(adviceRecordList, ar);
    }

    private void objectsPut(String id, ObjectRecord td) {
        final ObjectRecord old = objects.put(td.getId(), td);
        assert old == null;
        // remember max epoch for resolving unqualified ids
        maxAllocationEpoch.put(id, allocationEpoch.epoch);
    }

    /**
     * Gets the {@link ObjectRecord} associated with an unqualified id. The qualified id is found using the
     * {@link #maxEpochMap}.
     *
     * @param id
     * @throws TraceException
     */
    private ObjectRecord getTraceRecord(String id) throws TraceException {
        if (id.equals("0")) {
            return null;
        }
        final Integer thisMaxAllocationEpoch = maxAllocationEpoch.get(id);
        assert thisMaxAllocationEpoch != null;
        final String uid = ObjectRecord.getMapId(id, thisMaxAllocationEpoch);
        ObjectRecord td = objects.get(uid);
        if (td == null) {
            throw new TraceException("no creation record for id = " + uid + ", at line " + lineNumber);
        }
        td.traceOccurrences++;
        return td;
    }

    /**
     * Sets (and returns) {@link #classRecord}, handling forward references.
     * In the latter case, the name field in the ClassRecord will be based on {@value CLASS_FORWARD_PREFIX}
     * and will be fixed up later.
     * @param shortClassName in the trace
     * @return a {@link ClassRecord}
     */
    private ClassRecord getClassRecord(String shortClassName) {
        ClassNameId classNameId = classShortFormsToFull.get(shortClassName);
        if (classNameId == null) {
            // forward reference
            classNameId = classForwardRefs.get(shortClassName);
            if (classNameId == null) {
                // new forward reference
                String forwardName = FORWARD_PREFIX + shortClassName;
                // the class definition will contain the classloader id;
                // at this stage we create a don't care value, as it isn't used
                classNameId = new ClassNameId(forwardName, ObjectRecord.getMapId("0", allocationEpoch.epoch));
                classForwardRefs.put(shortClassName, classNameId);
            }
        }
        // Have we created a ClassRecord for this class?
        classRecord = classMap.get(classNameId);
        if (classRecord == null) {
            classRecord = new ClassRecord(classNameId.className, classNameId.clId);
            classMap.put(classNameId, classRecord);
        }
        return classRecord;
    }

    /*
     */

    /**
     * Sets (and returns) {@link #fieldRecord} handling forward references.
     * @param shortFieldName
     */
    private FieldRecord getFieldRecord(String shortFieldName) {
        fieldRecord = fieldShortFormHandler.getRecord(shortFieldName);
        return fieldRecord;
    }

    /**
     * Sets (and returns) {@link #methodRecord} handling forward references.
     * @param shortMethodName
     */
    private MethodRecord getMethodRecord(String shortMethodName) {
        methodRecord = methodShortFormHandler.getRecord(shortMethodName);
        return methodRecord;
    }

    /**
     * Class definition, may resolve a previous forward reference.
     */
    private void defineClass(String name, long classLoaderId, String shortForm) {
        // class definition
        ClassNameId classNameId = new ClassNameId(name, getClassLoaderIdAsString(classLoaderId));
        classShortFormsToFull.put(shortForm, classNameId);
        // fix up forward reference
        ClassNameId forwardName = classForwardRefs.get(shortForm);
        if (forwardName != null) {
            // find class record in the classMap under the forward name, remove it,
            // patch name/id and add back under new name.
            ClassRecord cr = classMap.remove(forwardName);
            assert cr != null;
            cr.setName(name, getClassLoaderIdAsString(classLoaderId));
            classMap.put(classNameId, cr);
        }
    }

    private String getClassLoaderIdAsString(long clId) {
        String clIdString = Long.toString(clId);
        Integer epoch = maxAllocationEpoch.get(clIdString);
        if (epoch == null) {
            // This is a special case for the self referring bootstrap class loader
            epoch = allocationEpoch.epoch;
        }
        return ObjectRecord.getMapId(Long.toString(clId), epoch);
    }

    private void defineField() {
        fieldShortFormHandler.define();
    }

    private void defineMethod() {
        methodShortFormHandler.define();
    }

    private long getTime() throws TraceException {
        long timeValue = Long.parseLong(recordParts[TIME_INDEX]);
        if (absTime) {
            lastTime = timeValue;
        } else {
            lastTime += timeValue;
        }
        return lastTime;
    }

    private long expectNumber(String arg) throws TraceException {
        if (!(arg.charAt(0) >= '0' || arg.charAt(0) <= '9')) {
            throw new TraceException("number expected at line " + lineNumber);
        } else {
            return Long.parseLong(arg);
        }
    }

    private Key expectValidTraceStart() throws TraceException {
        Key result = VMATextStoreFormat.getCommand(textKeyMode, recordParts[KEY_INDEX]);
        if (result == null) {
            throw new TraceException("unknown trace command at line " + lineNumber);
        } else {
            return result;
        }
    }

    private void getTimeAndThread() throws TraceException {
        getTime();
        // N.B. there can be no forward references to threads
        String threadName = threadShortFormsToFull.get(recordParts[THREAD_INDEX]);
        assert threadName != null;
        threadRecord = threadMap.get(threadName);

    }

    private String arg(int slot) {
        if (slot < recordParts.length) {
            return recordParts[slot];
        } else {
            return null;
        }
    }

    @SuppressWarnings("fallthrough")
    private void processTraceRecord() throws TraceException {
        String arg1 = arg(1);
        String arg2 = arg(2);
        String bciArg = arg(3);
        String threadArg = arg2;
        String objIdArg = "???";
        int adviceModeInt = -1;

        Key key = VMATextStoreFormat.getCommand(textKeyMode, recordParts[0]);

        if (VMATextStoreFormat.hasTimeAndThread(key)) {
            getTimeAndThread();
        } else if (VMATextStoreFormat.hasTime(key)) {
            getTime();
        }

        if (VMATextStoreFormat.hasBci(key)) {
            bci = (short) Integer.parseInt(bciArg);
        }

        if (VMATextStoreFormat.hasId(key)) {
            if (arg(OBJ_ID_INDEX).charAt(0) == REPEAT_ID) {
                objIdArg = lastId.get(threadArg);
            } else {
                objIdArg = arg(OBJ_ID_INDEX);
                lastId.put(threadArg, objIdArg);
            }
        }

        AdviceRecord adviceRecord = null;

        switch (key) {
            case INITIALIZE_STORE:
                startTime = Long.parseLong(arg1);
                lastTime = startTime;
                allocationEpoch = new AllocationEpoch(startTime);
                allocationEpochs.add(allocationEpoch);
                absTime = Boolean.parseBoolean(arg2);
                return;

            case FINALIZE_STORE: {
                long t = Long.parseLong(arg1);
                lastTime = absTime ? t : lastTime + t;
                allocationEpoch.endTime = lastTime;
                return;
            }

            case THREAD_SWITCH:
                throw new TraceException("batched log is not supported - use ConvertLog -unbatch");

            case CLASS_DEFINITION: {
                defineClass(ClassRecord.getCanonicalName(recordParts[DEFINE_ARG_INDEX]),
                                expectNumber(recordParts[DEFINE_ARG_INDEX + 1]), recordParts[DEFINE_ARG_INDEX + 2]);
                return;
            }

            case FIELD_DEFINITION: {
                defineField();
                return;
            }

            case METHOD_DEFINITION: {
                defineMethod();
                return;
            }

            case THREAD_DEFINITION: {
                ThreadRecord tr = new ThreadRecord(recordParts[DEFINE_ARG_INDEX]);
                threadMap.put(tr.name, tr);
                threadShortFormsToFull.put(recordParts[DEFINE_ARG_INDEX + 1], tr.name);
                return;
            }

            case ADVISE_BEFORE_THROW:
            case ADVISE_BEFORE_MONITOR_ENTER:
            case ADVISE_BEFORE_MONITOR_EXIT: {
                objectRecord = getTraceRecord(objIdArg);
                ObjectAdviceRecord objectAdviceRecord = (ObjectAdviceRecord) createAdviceRecordAndSetTimeAndThread(keyToRecordType(key), AdviceMode.BEFORE, bci);
                objectAdviceRecord.value = objectRecord;
                objectRecord.addTraceElement(objectAdviceRecord);
                adviceRecord = objectAdviceRecord;
                break;
            }

            case UNSEEN:
            case ADVISE_AFTER_NEW_ARRAY:
            case ADVISE_AFTER_NEW: {
                ObjectAdviceRecord objectAdviceRecord = (ObjectAdviceRecord) createAdviceRecordAndSetTimeAndThread(keyToRecordType(key), AdviceMode.AFTER, bci);
                getClassRecord(recordParts[NEW_CLASSNAME_INDEX]);
                objectRecord = new ObjectRecord(objIdArg, allocationEpoch.epoch, classRecord, threadRecord, objectAdviceRecord);
                classRecord.addObject(objectRecord);
                objectsPut(objIdArg, objectRecord);
                objectAdviceRecord.value = objectRecord;
                if (key == Key.UNSEEN) {
                    // We don't know how or when this was constructed, but give it an end creation time of now.
                    objectRecord.setEndCreationRecord(objectAdviceRecord);
                    missingConstructors.put(objectRecord.getId(), objectRecord);
                    missingConstructorCount++;
                } else if (key == Key.ADVISE_AFTER_NEW_ARRAY) {
                    objectAdviceRecord.setPackedValue(Integer.parseInt(arg(NEW_ARRAY_LENGTH_INDEX))); // array length
                    objectRecord.setEndCreationRecord(objectAdviceRecord);
                }
                if (classRecord.isArray()) {
                    arrayCount++;
                } else {
                    objectCount++;
                }
                adviceRecord = objectAdviceRecord;
                break;
            }

            case ADVISE_BEFORE_CONST_LOAD: {
                adviceRecord = createAdviceRecordAndSetTimeThreadValue("ConstLoad", AdviceMode.BEFORE, arg(CONST_LOAD_VALUE_INDEX), arg(CONST_LOAD_VALUE_INDEX + 1));
                break;
            }

            case ADVISE_BEFORE_LOAD: {
                adviceRecord = createAdviceRecordAndSetTimeAndThread(Load, AdviceMode.BEFORE, bci);
                adviceRecord.setPackedValue(Integer.parseInt(arg(LOADSTORE_DISP_INDEX)));
                break;
            }

            case ADVISE_AFTER_LOAD:
            case ADVISE_BEFORE_STORE: {
                adviceRecord = createAdviceRecordAndSetTimeThreadValue(key == Key.ADVISE_AFTER_LOAD ? "Load" : "Store", AdviceMode.BEFORE, arg(LOADSTORE_DISP_INDEX + 1), arg(LOADSTORE_DISP_INDEX + 2));
                if (adviceRecord.getRecordType() == StoreObject || adviceRecord.getRecordType() == LoadObject) {
                    ObjectRecord or = AdviceRecordHelper.getObjectRecord(adviceRecord);
                    if (or != null) {
                        or.addTraceElement(adviceRecord);
                    }
                }
                adviceRecord.setPackedValue(Integer.parseInt(arg(LOADSTORE_DISP_INDEX)));
                break;
            }

            case ADVISE_BEFORE_ARRAY_LOAD: {
                objectRecord = getTraceRecord(objIdArg);
                int arrayIndex = (int) expectNumber(arg(ARRAY_INDEX_INDEX));
                ObjectAdviceRecord objectAdviceRecord = (ObjectAdviceRecord) createAdviceRecordAndSetTimeAndThread(keyToRecordType(key), AdviceMode.BEFORE, bci);
                objectAdviceRecord.value = objectRecord;
                objectAdviceRecord.setPackedValue(arrayIndex);
                objectRecord.addTraceElement(objectAdviceRecord);
                adviceRecord = objectAdviceRecord;
                break;
            }

            case ADVISE_AFTER_ARRAY_LOAD:
            case ADVISE_BEFORE_ARRAY_STORE: {
                objectRecord = getTraceRecord(objIdArg);
                int arrayIndex = (int) expectNumber(arg(ARRAY_INDEX_INDEX));
                ObjectAdviceRecord objectAdviceRecord = (ObjectAdviceRecord) createAdviceRecordAndSetTimeThreadValue(
                                key == Key.ADVISE_BEFORE_ARRAY_STORE ? "ArrayStore" : "ArrayLoad",
                                AdviceMode.BEFORE, arg(ARRAY_INDEX_INDEX + 1), arg(ARRAY_INDEX_INDEX + 2));
                objectAdviceRecord.value = objectRecord;
                objectAdviceRecord.setPackedValue(arrayIndex);
                objectRecord.addTraceElement(objectAdviceRecord);
                if (objectAdviceRecord.getRecordType() == ArrayStoreObject || objectAdviceRecord.getRecordType() == ArrayLoadObject) {
                    ObjectRecord object1 = getTraceRecord(arg(ARRAY_INDEX_INDEX + 2));
                    if (object1 != null) {
                        object1.addTraceElement(objectAdviceRecord);
                    }

                }
                adviceRecord = objectAdviceRecord;
                break;
            }

            case ADVISE_AFTER_ARRAY_LENGTH: {
                objectRecord = getTraceRecord(objIdArg);
                ObjectAdviceRecord objectAdviceRecord = (ObjectAdviceRecord) createAdviceRecordAndSetTimeAndThread(ArrayLength, AdviceMode.BEFORE, bci);
                objectAdviceRecord.value = objectRecord;
                objectAdviceRecord.setPackedValue(Integer.parseInt(arg(ARRAY_LENGTH_INDEX)));
                objectRecord.addTraceElement(objectAdviceRecord);
                adviceRecord = objectAdviceRecord;
                break;
            }


            case ADVISE_BEFORE_GET_STATIC: {
                getFieldRecord(arg(STATIC_FIELDNAME_INDEX));
                ObjectFieldAdviceRecord objectFieldAdviceRecord = (ObjectFieldAdviceRecord) createAdviceRecordAndSetTimeAndThread(GetStatic, AdviceMode.BEFORE, bci);
                objectFieldAdviceRecord.value = classRecord;
                objectFieldAdviceRecord.field = fieldRecord;
                classRecord.addTraceElement(objectFieldAdviceRecord);
                adviceRecord = objectFieldAdviceRecord;
                break;
            }

            case ADVISE_BEFORE_PUT_STATIC: {
                getFieldRecord(arg(STATIC_FIELDNAME_INDEX));
                ObjectFieldAdviceRecord objectFieldAdviceRecord = (ObjectFieldAdviceRecord) createAdviceRecordAndSetTimeThreadValue("PutStatic", AdviceMode.BEFORE,
                                arg(STATIC_FIELDNAME_INDEX + 1), arg(STATIC_FIELDNAME_INDEX + 2));
                objectFieldAdviceRecord.value = classRecord;
                objectFieldAdviceRecord.field = fieldRecord;
                classRecord.addTraceElement(objectFieldAdviceRecord);
                adviceRecord = objectFieldAdviceRecord;
                break;
            }

            case ADVISE_BEFORE_GET_FIELD: {
                objectRecord = getTraceRecord(objIdArg);
                getFieldRecord(arg(ID_FIELDNAME_INDEX));
                ObjectFieldAdviceRecord objectFieldAdviceRecord = (ObjectFieldAdviceRecord) createAdviceRecordAndSetTimeAndThread(GetField, AdviceMode.BEFORE, bci);
                objectFieldAdviceRecord.value = objectRecord;
                objectFieldAdviceRecord.field = fieldRecord;
                objectRecord.addTraceElement(objectFieldAdviceRecord);
                adviceRecord = objectFieldAdviceRecord;
                break;
            }

            case ADVISE_BEFORE_PUT_FIELD: {
                objectRecord = getTraceRecord(objIdArg);
                getFieldRecord(arg(ID_FIELDNAME_INDEX));
                ObjectFieldAdviceRecord objectFieldAdviceRecord = (ObjectFieldAdviceRecord) createAdviceRecordAndSetTimeThreadValue("PutField", AdviceMode.BEFORE,
                                arg(ID_FIELDNAME_INDEX + 1), arg(ID_FIELDNAME_INDEX + 2));
                objectFieldAdviceRecord.value = objectRecord;
                objectFieldAdviceRecord.field = fieldRecord;
                objectRecord.addTraceElement(objectFieldAdviceRecord);
                adviceRecord = objectFieldAdviceRecord;
                break;
            }

            case ADVISE_BEFORE_IF: {
                if (arg(IF_OPCODE_INDEX + 1).equals("J")) {
                    LongLongTBciAdviceRecord longLongAdviceRecord = (LongLongTBciAdviceRecord) createAdviceRecordAndSetTimeAndThread(IfInt, AdviceMode.BEFORE, bci);
                    longLongAdviceRecord.value = Long.parseLong(arg(IF_OPCODE_INDEX + 2));
                    longLongAdviceRecord.value2 = Long.parseLong(arg(IF_OPCODE_INDEX + 3));
                    longLongAdviceRecord.targetBci = Short.parseShort(arg(IF_OPCODE_INDEX + 4));
                    adviceRecord = longLongAdviceRecord;
                } else {
                    ObjectObjectTBciAdviceRecord objectObjectAdviceRecord = (ObjectObjectTBciAdviceRecord) createAdviceRecordAndSetTimeAndThread(IfObject, AdviceMode.BEFORE, bci);
                    ObjectRecord object1 = getTraceRecord(arg(IF_OPCODE_INDEX + 2));
                    ObjectRecord object2 = getTraceRecord(arg(IF_OPCODE_INDEX + 3));
                    objectObjectAdviceRecord.value = object1;
                    objectObjectAdviceRecord.value2 = object2;
                    objectObjectAdviceRecord.targetBci = Short.parseShort(arg(IF_OPCODE_INDEX + 4));
                    if (object1 != null) {
                        object1.addTraceElement(objectObjectAdviceRecord);
                    }
                    if (object2 != null) {
                        object2.addTraceElement(objectObjectAdviceRecord);
                    }
                    adviceRecord = objectObjectAdviceRecord;
                }
                adviceRecord.setPackedValue(Integer.parseInt(arg(IF_OPCODE_INDEX)));
                break;
            }

            case ADVISE_BEFORE_OPERATION: {
                adviceRecord = createAdviceRecordAndSetTimeThreadValue("Operation", AdviceMode.BEFORE, arg(OP_VALUES_INDEX), arg(OP_VALUES_INDEX + 1));
                adviceRecord.setPackedValue(Integer.parseInt(arg(OP_OPCODE_INDEX)));
                switch (arg(OP_VALUES_INDEX).charAt(0)) {
                    case LONG_VALUE:
                        ((LongLongAdviceRecord) adviceRecord).value2 = Long.parseLong(arg(OP_VALUES_INDEX + 2));
                        break;
                    case FLOAT_VALUE:
                        ((FloatFloatAdviceRecord) adviceRecord).value2 = Float.parseFloat(arg(OP_VALUES_INDEX + 2));
                        break;
                    case DOUBLE_VALUE:
                        ((DoubleDoubleAdviceRecord) adviceRecord).value2 = Double.parseDouble(arg(OP_VALUES_INDEX + 2));
                        break;
                    default:
                        throw new IllegalArgumentException("bad type " + arg(OP_VALUES_INDEX).charAt(0) + " in value");
                }
                break;
            }

            case ADVISE_BEFORE_INSTANCE_OF:
            case ADVISE_BEFORE_CHECK_CAST:  {
                objectRecord = getTraceRecord(objIdArg);
                getClassRecord(arg(ID_CLASSNAME_INDEX));
                ObjectObjectAdviceRecord objectObjectAdviceRecord = (ObjectObjectAdviceRecord) createAdviceRecordAndSetTimeAndThread(keyToRecordType(key), AdviceMode.BEFORE, bci);
                objectObjectAdviceRecord.value = objectRecord;
                objectObjectAdviceRecord.value2 = classRecord;
                if (objectRecord != null) {
                    objectRecord.addTraceElement(objectObjectAdviceRecord);
                }
                adviceRecord = objectObjectAdviceRecord;
                break;
            }

            case ADVISE_BEFORE_CONVERSION: {
                adviceRecord = createAdviceRecordAndSetTimeThreadValue("Conversion", AdviceMode.BEFORE, arg(CONV_OPCODE_INDEX + 1), arg(CONV_OPCODE_INDEX + 2));
                adviceRecord.setPackedValue(Integer.parseInt(arg(CONV_OPCODE_INDEX)));
                break;
            }

            case ADVISE_BEFORE_GC: {
                allocationEpoch.setEndTime(lastTime);
                adviceRecord = createAdviceRecordAndSetTimeAndThread(keyToRecordType(key), AdviceMode.BEFORE, bci);
                break;
            }

            case ADVISE_AFTER_GC: {
                prevAllocationEpoch = allocationEpoch;
                allocationEpoch = new AllocationEpoch(lastTime);
                allocationEpochs.add(allocationEpoch);
                adviceRecord = createAdviceRecordAndSetTimeAndThread(keyToRecordType(key), AdviceMode.AFTER, bci);
                break;
            }

            case REMOVAL: {
                if (startRemoval < 0) {
                    startRemoval = adviceRecordList.size();
                }
                endRemoval = adviceRecordList.size();
                objIdArg = arg1;
                // This object may have been created in some earlier epoch, so can't use current.
                // It must be the death of id recorded by maxAllocationEpoch
                objectRecord = objects.get(ObjectRecord.getMapId(objIdArg, maxAllocationEpoch.get(objIdArg)));
                ObjectAdviceRecord objectAdviceRecord = (ObjectAdviceRecord) createAdviceRecordAndSetTimeAndThread(Removal, AdviceMode.AFTER, bci);
                objectAdviceRecord.value = objectRecord;
                objectRecord.setRemovalRecord(objectAdviceRecord);
                adviceRecord = objectAdviceRecord;
                break;
            }

            case ADVISE_BEFORE_INVOKE_INTERFACE:
            case ADVISE_BEFORE_INVOKE_STATIC:
            case ADVISE_BEFORE_INVOKE_VIRTUAL:
            case ADVISE_BEFORE_INVOKE_SPECIAL:
                adviceModeInt = AdviceMode.BEFORE.ordinal();
            // Checkstyle: stop
                /*
            case ADVISE_AFTER_INVOKE_INTERFACE:
            case ADVISE_AFTER_INVOKE_STATIC:
            case ADVISE_AFTER_INVOKE_VIRTUAL:
            case ADVISE_AFTER_INVOKE_SPECIAL:
            */
            case ADVISE_AFTER_METHOD_ENTRY: {
                // Checkstyle: resume
                if (adviceModeInt == -1) {
                    adviceModeInt = AdviceMode.AFTER.ordinal();
                }
                objectRecord = getTraceRecord(objIdArg);
                getMethodRecord(arg(ID_MEMBERNAME_INDEX));
                ObjectMethodAdviceRecord objectAdviceRecord = (ObjectMethodAdviceRecord) createAdviceRecordAndSetTimeAndThread(keyToRecordType(key), AdviceMode.values()[adviceModeInt], bci);
                if (key == Key.ADVISE_BEFORE_INVOKE_STATIC /*|| key == Key.ADVISE_AFTER_INVOKE_STATIC*/) {
                    objectAdviceRecord.value = classRecord;
                } else {
                    objectAdviceRecord.value = objectRecord;
                    if (objectRecord != null) {
                        objectRecord.addTraceElement(objectAdviceRecord);
                    }
                }
                objectAdviceRecord.value2 = methodRecord;
                /*
                if (key == Key.ADVISE_AFTER_INVOKE_SPECIAL) {
                    if (methodRecord.name.equals("<init>")) {
                        objectRecord.setEndCreationRecord(objectAdviceRecord);
                    }
                }
                */
                adviceRecord = objectAdviceRecord;
                break;
            }

            case ADVISE_BEFORE_RETURN: {
                if (arg(RETURN_VALUE_INDEX) != null) {
                    adviceRecord = createAdviceRecordAndSetTimeThreadValue("Return", AdviceMode.BEFORE, arg(RETURN_VALUE_INDEX), arg(RETURN_VALUE_INDEX + 1));
                    if (adviceRecord.getRecordType() == ReturnObject) {
                        ObjectRecord or = AdviceRecordHelper.getObjectRecord(adviceRecord);
                        if (or != null) {
                            or.addTraceElement(adviceRecord);
                        }
                    }
                } else {
                    adviceRecord = createAdviceRecordAndSetTimeAndThread(Return, AdviceMode.BEFORE, bci);
                }
                break;
            }

            case ADVISE_BEFORE_RETURN_BY_THROW: {
                objectRecord = getTraceRecord(objIdArg);
                ObjectLongAdviceRecord objectAdviceRecord = (ObjectLongAdviceRecord) createAdviceRecordAndSetTimeAndThread(ReturnByThrow, AdviceMode.BEFORE, bci);
                objectAdviceRecord.setPackedValue(Integer.parseInt(arg(RETURN_THROW_POP_INDEX)));
                objectAdviceRecord.value = objectRecord;
                adviceRecord = objectAdviceRecord;
                break;
            }

            case ADVISE_BEFORE_STACK_ADJUST: {
                adviceRecord = createAdviceRecordAndSetTimeAndThread(keyToRecordType(key), AdviceMode.BEFORE, bci);
                adviceRecord.setPackedValue(Integer.parseInt(arg(STACK_ADJUST_INDEX)));
                break;
            }

            case ADVISE_BEFORE_THREAD_TERMINATING:
            case ADVISE_BEFORE_THREAD_STARTING:
                adviceRecord = createAdviceRecordAndSetTimeAndThread(keyToRecordType(key), AdviceMode.BEFORE, bci);
                if (key == Key.ADVISE_BEFORE_THREAD_STARTING) {
                    threadRecord.startTime = lastTime;
                } else {
                    threadRecord.endTime = lastTime;
                }
                break;

            case ADVISE_AFTER_MULTI_NEW_ARRAY:
                assert false : key + " unexpected";
                break;

            case ADVISE_BEFORE_GOTO:
                adviceRecord = createAdviceRecordAndSetTimeAndThread(keyToRecordType(key), AdviceMode.BEFORE, bci);
                adviceRecord.setPackedValue(Integer.parseInt(arg(GOTO_TARGET_INDEX)));
                break;

            default:
                throw new TraceException("unimplemented key: " + key);
        }


        if (key != Key.REMOVAL && startRemoval >= 0) {
            prevAllocationEpoch.setRemovalRange(startRemoval, endRemoval);
            startRemoval = -1;
        }
        assert adviceRecord != null;
        adviceRecordList.add(adviceRecord);
    }

    private AdviceRecord createAdviceRecordAndSetTimeThreadValue(String rtPrefix, AdviceMode adviceMode, String valueKey, String value) throws TraceException {
        RecordType rt = getRecordTypeForValue(rtPrefix, valueKey);
        AdviceRecord adviceRecord = createAdviceRecordAndSetTimeAndThread(rt, adviceMode, bci);
        switch (valueKey.charAt(0)) {
            case OBJ_VALUE: {
                ObjectRecord or = getTraceRecord(value);
                switch (rt) {
                    case PutFieldObject:
                    case PutStaticObject:
                        ((ObjectFieldObjectAdviceRecord) adviceRecord).value = or;
                        break;
                    case ConstLoadObject:
                    case StoreObject:
                    case LoadObject:
                    case ReturnObject:
                        ((ObjectAdviceRecord) adviceRecord).value = or;
                        break;
                    default:
                        ((ObjectObjectAdviceRecord) adviceRecord).value2 = or;
                }
                break;
            }

            case LONG_VALUE: {
                long v = Long.parseLong(value);
                switch (rt) {
                    case PutFieldLong:
                    case PutStaticLong:
                        ((ObjectFieldLongAdviceRecord) adviceRecord).value2 = v;
                        break;
                    case ConstLoadLong:
                    case StoreLong:
                    case ConversionLong:
                    case ReturnLong:
                        ((LongAdviceRecord) adviceRecord).value = v;
                        break;
                    case OperationLong:
                        ((LongLongAdviceRecord) adviceRecord).value = v;
                        break;
                    default:
                        ((ObjectLongAdviceRecord) adviceRecord).value2 = v;
                }
                break;
            }

            case FLOAT_VALUE: {
                float v = Float.parseFloat(value);
                switch (rt) {
                    case PutFieldFloat:
                    case PutStaticFloat:
                        ((ObjectFieldFloatAdviceRecord) adviceRecord).value2 = v;
                        break;
                    case ConstLoadFloat:
                    case StoreFloat:
                    case ConversionFloat:
                    case ReturnFloat:
                        ((FloatAdviceRecord) adviceRecord).value = v;
                        break;
                    case OperationFloat:
                        ((FloatFloatAdviceRecord) adviceRecord).value = v;
                        break;
                    default:
                        ((ObjectFloatAdviceRecord) adviceRecord).value2 = v;
                }
                break;
            }

            case DOUBLE_VALUE: {
                double v = Double.parseDouble(value);
                switch (rt) {
                    case PutFieldDouble:
                    case PutStaticDouble:
                        ((ObjectFieldDoubleAdviceRecord) adviceRecord).value2 = v;
                        break;
                    case ConstLoadDouble:
                    case StoreDouble:
                    case ConversionDouble:
                    case ReturnDouble:
                        ((DoubleAdviceRecord) adviceRecord).value = v;
                        break;
                    case OperationDouble:
                        ((DoubleDoubleAdviceRecord) adviceRecord).value = v;
                        break;
                    default:
                        ((ObjectDoubleAdviceRecord) adviceRecord).value2 = v;
                }
                break;
            }

            default:
                assert false;

        }
        return adviceRecord;
    }

    private AdviceRecord createAdviceRecordAndSetTimeAndThread(RecordType rt, AdviceMode adviceMode, short bci) {
        AdviceRecord adviceRecord;
        // Handle the change of record type for PUT/GET/FIELD/STATIC and REMOVAL types
        switch (rt) {
            case PutFieldLong:
            case PutStaticLong:
                adviceRecord = new ObjectFieldLongAdviceRecord();
                break;
            case PutFieldFloat:
            case PutStaticFloat:
                adviceRecord = new ObjectFieldFloatAdviceRecord();
                break;
            case PutFieldDouble:
            case PutStaticDouble:
                adviceRecord = new ObjectFieldDoubleAdviceRecord();
                break;

            case PutFieldObject:
            case PutStaticObject:
                adviceRecord = new ObjectFieldObjectAdviceRecord();
                break;

            case GetField:
            case GetStatic:
                adviceRecord = new ObjectFieldAdviceRecord();
                break;

            case Removal:
                adviceRecord = new ObjectAdviceRecord();
                break;

            default:
                adviceRecord = rt.newAdviceRecord();
        }
        adviceRecord.setCodeModeBci(rt, adviceMode, bci);
        adviceRecord.time = lastTime;
        adviceRecord.thread = threadRecord;
        return adviceRecord;
    }

    private static RecordType getRecordTypeForValue(String rtPrefix, String valueKey) {
        String suffix;
        switch (valueKey.charAt(0)) {
            case OBJ_VALUE:
                suffix = "Object";
                break;
            case LONG_VALUE:
                suffix = "Long";
                break;
            case FLOAT_VALUE:
                suffix = "Float";
                break;
            case DOUBLE_VALUE:
                suffix = "Double";
                break;
            default:
                throw new IllegalArgumentException("bad type " + valueKey.charAt(0) + " in value");
        }
        return RecordType.valueOf(rtPrefix + suffix);
    }

    private static RecordType keyToRecordType(Key key) throws TraceException {
        switch (key) {
            case ADVISE_BEFORE_THROW:
                return Throw;
            case ADVISE_BEFORE_INSTANCE_OF:
                return InstanceOf;
            case ADVISE_AFTER_ARRAY_LENGTH:
                return ArrayLength;
            case ADVISE_BEFORE_CHECK_CAST:
                return CheckCast;
            case ADVISE_BEFORE_GET_STATIC:
                return GetStatic;
            case ADVISE_BEFORE_GET_FIELD:
                return GetField;

            case ADVISE_BEFORE_MONITOR_ENTER:
                return MonitorEnter;
            case ADVISE_BEFORE_MONITOR_EXIT:
                return MonitorExit;

            /*case ADVISE_AFTER_INVOKE_INTERFACE:*/
            case ADVISE_BEFORE_INVOKE_INTERFACE:
                return InvokeInterface;
            /*case ADVISE_AFTER_INVOKE_STATIC:*/
            case ADVISE_BEFORE_INVOKE_STATIC:
                return InvokeStatic;
            /*case ADVISE_AFTER_INVOKE_SPECIAL:*/
            case ADVISE_BEFORE_INVOKE_SPECIAL:
                return InvokeSpecial;
            case ADVISE_BEFORE_INVOKE_VIRTUAL:
            /*case ADVISE_AFTER_INVOKE_VIRTUAL:*/
                return InvokeVirtual;

            case ADVISE_AFTER_METHOD_ENTRY:
                return MethodEntry;

            case ADVISE_BEFORE_STACK_ADJUST:
                return StackAdjust;

            case ADVISE_AFTER_GC:
            case ADVISE_BEFORE_GC:
                return GC;
            case ADVISE_BEFORE_THREAD_TERMINATING:
                return ThreadTerminating;
            case ADVISE_BEFORE_THREAD_STARTING:
                return ThreadStarting;

            case ADVISE_BEFORE_ARRAY_LOAD:
                return ArrayLoad;

            case ADVISE_AFTER_NEW:
                return New;
            case ADVISE_AFTER_NEW_ARRAY:
                return NewArray;

            case REMOVAL:
                return Removal;
            case UNSEEN:
                return Unseen;

            case ADVISE_BEFORE_RETURN:
                return Return;

            case ADVISE_BEFORE_GOTO:
                return Goto;

            case ADVISE_AFTER_MULTI_NEW_ARRAY:
                // These are all value based so do not have a simple static mapping
            case ADVISE_BEFORE_CONVERSION:
            case ADVISE_BEFORE_PUT_FIELD:
            case ADVISE_BEFORE_PUT_STATIC:
            case ADVISE_BEFORE_ARRAY_STORE:
            case ADVISE_AFTER_ARRAY_LOAD:
            case ADVISE_BEFORE_IF:
            case ADVISE_BEFORE_OPERATION:
            case ADVISE_BEFORE_LOAD:
            case ADVISE_AFTER_LOAD:
            case ADVISE_BEFORE_STORE:
            case ADVISE_BEFORE_CONST_LOAD:

        }
        throw new TraceException("unimplemented case");
    }

    public static void main(String[] args) {
        QueryAnalysis.main(args);
    }

}

